import Konva from "konva"
import AsyncPreloader from "async-preloader"
import _ from "lodash"

const position = {
    left: "l",
    right: "r",
    top: "t",
    bottom: "b"
}

const jewelState = {
    plain: "p",
    strike: "s",
    blow: "b"
}

const containerElementId = "jewels",
    jewelGap = 10,
    jewelSize = [60, 60],
    jewelRowCount = 10,
    jewelColCount = 10,
    swapDeltaThreshold = 5,
    idPrefix = "jewel",
    separator = "-",
    typeNamePrefix = "type",
    typeCount = 4,                             // set it at least greater than 2 :)
    newJewelAddPosition = position.bottom,     // drop to position
    jewelTweenDuration = 0.2




const jewelAssets = _.reduce(_.range(8), (result, i) => {
    result.push(...[
        { id: `${i + 1}p`, src: `static/image/${i + 1}p.png` },
        { id: `${i + 1}s`, src: `static/image/${i + 1}s.png` },
        { id: `${i + 1}b`, src: `static/image/${i + 1}b.png` }
    ])
    return result
}, [])

const imageAssets = [
    { id: `logo`, src: "static/image/logo.png" },
    { id: `mh`, src: "static/image/mh.png" },
    { id: `mf`, src: "static/image/mf.png" },
    { id: `g1`, src: "static/image/g1.png" },
    { id: `g2`, src: "static/image/g2.png" }
]

const effectAssets = [
    { id: `es`, src: "static/video/effect-strike.mp4" },
    { id: `eb`, src: "static/video/effect-blow.mp4" },
    { id: `flash`, src: "static/video/flash.mp4" },
]

const soundAssets = [
    { id: "jc", src: "static/sound/jc.mp3", body: "arrayBuffer" },
    { id: "sc", src: "static/sound/sc.mp3", body: "arrayBuffer" },
    { id: "bc", src: "static/sound/bc.mp3", body: "arrayBuffer" }
]

// AsyncPreloader use FontFaceObserver to detect font loading which defined in `fontStyle` below ,
// we should not specify `src` here
const fontAsset = { id: "pacifico", loader: "Font", src: "" }


const dimensions = {
    menuWidth: 462,
}

const audioContext = new AudioContext()

const Jewels = function () {
    this.init()
}


Jewels.prototype.initGlobalStyle = function () {

    let fontStyle = document.createElement("style")
    fontStyle.textContent = `
    @font-face {
        font-family: "pacifico";
        font-style: normal;
        font-weight: 400;
        font-display: swap;
        src: url(./static/font/Pacifico-Regular.ttf) format("truetype");
    }
    `
    let globalStyle = document.createElement("style")
    globalStyle.textContent = `
    html, body {
        margin: 0;
        padding: 0;
        height: 100%;
    }
    #${containerElementId} {
        cursor: url(./static/cursor/default.png), default;
    }
    #${containerElementId}.active{
        cursor: url(./static/cursor/pointer.png), pointer;
    }
    `

    document.head.appendChild(fontStyle)
    document.head.appendChild(globalStyle)
}

Jewels.prototype.activeCursor = function () {
    this.containerElement.classList.add("active")
}

Jewels.prototype.deactiveCursor = function () {
    this.containerElement.classList.remove("active")
}

Jewels.prototype.init = function () {
    this.containerElement = document.createElement("div")
    this.containerElement.id = containerElementId
    document.body.appendChild(this.containerElement)

    this.initGlobalStyle()

    this.resizeCallback = []


    this.stage = new Konva.Stage({
        container: containerElementId,
        width: window.innerWidth,
        height: window.innerHeight
    })


    this.layer = new Konva.Layer({ x: dimensions.menuWidth, y: 0 })
    this.stage.add(this.layer)

    this.menuLayer = new Konva.Layer()
    this.stage.add(this.menuLayer)



    this.initAssets().then(() => {
        this.initBgGroup()
        this.initMenuLayer()
        this.initJewels()
    })


    this._bindWindowResize()
}

Jewels.prototype.initMenuLayer = function () {
    let t = this

    t.menuLayer.add(new Konva.Rect({
        id: "menuBg",
        x: 0,
        y: 0,
        fill: "#28092B",
        width: dimensions.menuWidth,
        height: t.stage.height()
    }))


    t.menuLayer.add(new Konva.Image({
        id: "menuHead",
        x: 0,
        y: 0,
        width: dimensions.menuWidth,
        height: 401,
        image: t.imageAssets["mh"],
        shadowColor: "black",
        shadowBlur: 0,
        shadowOffset: { y: 7 },
        shadowOpacity: 0.5
    }))

    t.menuLayer.add(new Konva.Image({
        id: "logo",
        x: (dimensions.menuWidth - 100) / 2 - 80,
        y: 50,
        width: 100,
        height: 100,
        image: t.imageAssets["logo"],
        shadowColor: "#28092B",
        shadowBlur: 0,
        shadowOffset: { y: 3 },
        shadowOpacity: 0.5,
    }))

    t.menuLayer.add(new Konva.Text({
        id: "logoText",
        x: (dimensions.menuWidth - 100) / 2 + 40,
        y: 50 + 26,
        fontFamily: "pacifico",
        fontSize: 52,
        fillLinearGradientStartPoint: { x: 0, y: 0 },
        fillLinearGradientEndPoint: { x: 0, y: 52 },
        fillLinearGradientColorStops: [0, "#f2f2f2", 1, "#ededed"],
        shadowColor: "#28092B",
        shadowBlur: 0,
        shadowOffset: { y: 3 },
        shadowOpacity: 0.5,
        text: "Jewels"
    }))

    t.menuLayer.draw()

    t.resizeCallback.push(() => {
        t.menuLayer.findOne("#menuBg").height(t.stage.height())
    })
}


Jewels.prototype.initBgGroup = function () {
    let t = this

    t.bgGroup = new Konva.Group()
    t.layer.add(t.bgGroup)

    t.bgGroup.add(new Konva.Rect({
        id: "bgLayerBgRect",
        x: 0,
        y: 0,
        fill: "#28092B",
        width: t.layer.width(),
        height: t.layer.height()
    }))

    t.resizeCallback.push(() => {
        t.bgGroup.findOne("#bgLayerBgRect").size({
            width: this.layer.width(),
            height: this.layer.height()
        })
    })


    for (let i = 0; i < jewelRowCount; i++) {
        for (let j = 0; j < jewelColCount; j++) {
            t.bgGroup.add(
                new Konva.Image({
                    x: jewelSize[0] * j + jewelGap,
                    y: jewelSize[1] * i + jewelGap,
                    image: (j % 2) ^ (i % 2) ? t.imageAssets["g1"] : t.imageAssets["g2"],
                    opacity: 0.3,
                    width: jewelSize[0],
                    height: jewelSize[1]
                })
            )
        }
    }
    t.layer.draw()
}


Jewels.prototype._bindWindowResize = function () {
    window.addEventListener("resize", () => {
        this.stage.width(window.innerWidth)
        this.stage.height(window.innerHeight)
        _.forEach(this.resizeCallback, f => f())
    })
}

Jewels.prototype.getRandomJewel = function (exceptTypes) {
    let types = _.range(1, typeCount + 1) // 1 to typeCount

    if (exceptTypes) types = _.without(types, ...exceptTypes)

    let type = types[_.random(types.length - 1)]

    return {
        image: this.jewelAssets[`${type}${jewelState.plain}`],
        name: `${typeNamePrefix}${type} ${jewelState.plain}`,
    }
}

function parseId(id) {
    let arr = id.split(separator)
    return {
        row: Number(arr[1]),
        col: Number(arr[2])
    }
}

function stringifyId(row, col) {
    return `${idPrefix}${separator}${row}${separator}${col}`
}

Jewels.prototype.initJewels = function () {
    let t = this
    for (let i = 0; i < jewelRowCount; i++) {
        for (let j = 0; j < jewelColCount; j++) {

            // prevent clearable jewels on generation towards `top` and `left`
            let exceptTypes = [],
                leftOne = t._getJewelAt({ row: i, col: j - 1 }),
                leftTwo = t._getJewelAt({ row: i, col: j - 2 }),
                topOne = t._getJewelAt({ row: i - 1, col: j }),
                topTwo = t._getJewelAt({ row: i - 2, col: j })

            if (isSameType(leftOne, leftTwo)) exceptTypes.push(getJewelType(leftOne))
            if (isSameType(topOne, topTwo)) exceptTypes.push(getJewelType(topOne))

            let jewelAsset = t.getRandomJewel(exceptTypes)

            t.layer.add(t._jewelTemplate(i, j, jewelAsset.image, jewelAsset.name))
        }
    }
    t.layer.draw()
}

// TODO check if there is not any move for user to play
Jewels.prototype.checkIfNoMovesLeft = function () {

}

// TODO suggest the user what to do (`swapJewels` first, then `triggerJewel`)
Jewels.prototype.suggestNextMove = function () {

}

function calcJewelPosition(row, col) {
    return {
        x: jewelSize[0] * col + jewelGap,
        y: jewelSize[1] * row + jewelGap
    }
}

Jewels.prototype._jewelTemplate = function (row, col, image, name, isGroup = false) {
    let t = this,
        ret
    if (isGroup) {
        ret = new Konva.Group({
            ...calcJewelPosition(row, col),
            id: stringifyId(row, col),
            name: `${name} group`,
            width: jewelSize[0],
            height: jewelSize[1],
            draggable: true,
            dragDistance: swapDeltaThreshold,
            dragBoundFunc: function (pos) {
                return {
                    x: this.absolutePosition().x,
                    y: this.absolutePosition().y
                }
            }
        })

        let img = new Konva.Image({
            x: 0,
            y: 0,
            width: jewelSize[0],
            height: jewelSize[1],
            image: image
        })

        ret.add(img)
    } else {
        ret = new Konva.Image({
            ...calcJewelPosition(row, col),
            id: stringifyId(row, col),
            image: image,
            name: name,
            width: jewelSize[0],
            height: jewelSize[1],
            draggable: true,
            dragDistance: swapDeltaThreshold,
            dragBoundFunc: function (pos) {
                return {
                    x: this.absolutePosition().x,
                    y: this.absolutePosition().y
                }
            }
        })
    }

    ret.on("dragstart", function (e) {

    }).on("dragmove", function (e) {

        let deltaX = Math.abs(e.evt.movementX),
            deltaY = Math.abs(e.evt.movementY),
            p = deltaX > deltaY
                ? deltaX == e.evt.movementX ? position.right : position.left
                : deltaY == e.evt.movementY ? position.bottom : position.top

        let { row: fromRow, col: fromCol } = parseId(e.target.id()),
            toRow,
            toCol

        if (p == position.right) {
            if (fromCol == jewelColCount - 1) return
            [toRow, toCol] = [fromRow, fromCol + 1]
        }
        if (p == position.left) {
            if (fromCol == 0) return
            [toRow, toCol] = [fromRow, fromCol - 1]
        }
        if (p == position.bottom) {
            if (fromRow == jewelRowCount - 1) return
            [toRow, toCol] = [fromRow + 1, fromCol]
        }
        if (p == position.top) {
            if (fromRow == 0) return
            [toRow, toCol] = [fromRow - 1, fromCol]
        }

        t.swapJewels(fromRow, fromCol, toRow, toCol).then(() => {
            let clearable = t.checkClearable()
            if (clearable.length) {
                // do clear
                t.clearJewels(clearable)
            } else {
                // reverse
                t.swapJewels(toRow, toCol, fromRow, fromCol)
            }
        })
    }).on("dragend", function (e) {

    }).on("mouseover", function (e) {
        // TODO bug with effect video animation
        let group = e.target.findAncestor(".group")
        if (group) {
            t.activeCursor()
        }

        // e.target.cache()
        // e.target.brightness(0.1)
        // e.target.filters([Konva.Filters.Brighten])
        // t.layer.batchDraw()
    }).on("mouseout", function (e) {
        // e.target.brightness(0)
        // e.target.filters([])
        // t.layer.batchDraw()
        let group = e.target.findAncestor(".group")
        if (group) {
            t.deactiveCursor()
        }
    }).on("click", function (e) {

        let group = e.target.findAncestor(".group")
        if (group) {
            if (group.hasName(jewelState.blow) || group.hasName(jewelState.strike)) {
                t.triggerJewel(group)
            }
        }

    })

    return ret
}

Jewels.prototype.swapJewels = function (fromRow, fromCol, toRow, toCol) {

    let t = this,
        fromId = stringifyId(fromRow, fromCol),
        fromJewel = t.layer.findOne(`#${fromId}`),
        fromPosition = fromJewel.position(),

        toId = stringifyId(toRow, toCol),
        toJewel = t.layer.findOne(`#${toId}`),
        toPosition = toJewel.position()

    fromJewel.draggable(false) // disable draggable when swapping
    fromJewel.zIndex(toJewel.zIndex() + 1) // dragging jewel on the top when swap
    t.layer.listening(false)

    let fromPromise = new Promise((resolve, reject) => {
        fromJewel.to({
            duration: jewelTweenDuration,
            onFinish: () => {
                fromJewel.draggable(true) // enable draggable when swap complete
                t.layer.listening(true)
                t.layer.drawHit()
                fromJewel.id(toId)
                resolve()
            },
            x: toPosition.x,
            y: toPosition.y
        })
    })

    let toPromise = new Promise((resolve, reject) => {
        toJewel.to({
            duration: jewelTweenDuration,
            onFinish: () => {
                toJewel.id(fromId)
                resolve()
            },
            x: fromPosition.x,
            y: fromPosition.y
        })
    })

    //record swap to row,col as current row,col
    t.currentRowCol = [toRow, toCol]

    return Promise.all([fromPromise, toPromise])
}

Jewels.prototype._findAllClearingJewels = function (jewel, result) {
    let t = this,
        { row, col } = parseId(jewel.id())

    result.push(jewel)

    if (jewel.hasName(jewelState.blow)) {

        let type = getJewelType(jewel),
            sameTypeJewels = t.layer.find(node => {
                return node.hasName(`${typeNamePrefix}${type}`)
            })

        sameTypeJewels = _.without(sameTypeJewels, ...result)

        _.forEach(_.compact(sameTypeJewels), jewel => {
            if (jewel.hasName(jewelState.strike)) { // do not need other blow jewels, they have the same effect: clear all the same type jewels
                t._findAllClearingJewels(jewel, result)
            }
            result.push(jewel)
        })

    }
    if (jewel.hasName(jewelState.strike)) {
        let tlJewel = t.layer.findOne(`#${stringifyId(row - 1, col - 1)}`),
            tJewel = t.layer.findOne(`#${stringifyId(row - 1, col)}`),
            trJewel = t.layer.findOne(`#${stringifyId(row - 1, col + 1)}`),
            lJewel = t.layer.findOne(`#${stringifyId(row, col - 1)}`),
            rJewel = t.layer.findOne(`#${stringifyId(row, col + 1)}`),
            blJewel = t.layer.findOne(`#${stringifyId(row + 1, col - 1)}`),
            bJewel = t.layer.findOne(`#${stringifyId(row + 1, col)}`),
            brJewel = t.layer.findOne(`#${stringifyId(row + 1, col + 1)}`),
            aroundJewels = [tlJewel, tJewel, trJewel, lJewel, rJewel, blJewel, bJewel, brJewel]

        aroundJewels = _.without(aroundJewels, ...result)

        _.forEach(_.compact(aroundJewels), jewel => {
            if (jewel.hasName(jewelState.blow) || jewel.hasName(jewelState.strike)) {
                t._findAllClearingJewels(jewel, result)
            }
            result.push(jewel)
        })
    }
}

Jewels.prototype.triggerJewel = function (jewel) {

    let t = this,
        clearingJewels = [],
        promises

    t.layer.listening(false)
    t.deactiveCursor()

    t._findAllClearingJewels(jewel, clearingJewels)

    let isBlowClearing = _.filter(clearingJewels, j => j.hasName(jewelState.blow)).length > 0

    promises = t._clearJewels(clearingJewels)

    Promise.all(promises).then(() => {
        isBlowClearing ? t.playSound("bc") : t.playSound("sc")
        t.addNewJewels()
    })
}

Jewels.prototype._clearJewel = function (jewel) {
    if (jewel) {
        return new Promise((resolve, reject) => {
            jewel.to({
                opacity: 0,
                duration: jewelTweenDuration,
                onFinish: () => {
                    jewel.destroy()
                    resolve()
                }
            })
        })
    }
    return Promise.resolve()
}

Jewels.prototype._clearJewels = function (jewels) {
    let t = this,
        promises = []

    _.forEach(jewels, jewel => {
        if (jewel) {
            promises.push(t._clearJewel(jewel))
        }
    })

    if (promises.length) return promises

    return [Promise.resolve()]
}

Jewels.prototype._clearAndGenerateJewel = function (jewel, state) {
    let t = this,
        { row, col } = parseId(jewel.id()),
        type = getJewelType(jewel)

    if (jewel) {
        let newJewel = t._jewelTemplate(row, col, t.jewelAssets[`${type}${state}`], `${typeNamePrefix}${type} ${state}`, true)
        newJewel.opacity(0)
        let effectImage = new Konva.Image({
            x: 2,
            y: 2,
            width: jewelSize[0] - 4,
            height: jewelSize[1] - 4,
            globalCompositeOperation: "screen"
        })

        newJewel.add(effectImage)
        t.layer.add(newJewel)
        t.layer.draw()

        return Promise.all([
            t._clearJewel(jewel),
            new Promise((resolve, reject) => {
                newJewel.to({
                    opacity: 1,
                    duration: jewelTweenDuration,
                    onFinish: () => {
                        newJewel.id(stringifyId(row, col))
                        t.useEffect(`e${state}`, effectImage, t.layer)
                        resolve()
                    }
                })
            })
        ])
    }
    return Promise.resolve()
}

Jewels.prototype._getJewelAt = function ({ row, col, type, state } = {}) {
    let t = this

    if (row === undefined || col === undefined) {
        console.error("Row or col must be specified")
        return
    }

    return t.layer.findOne(node => {
        let judgement = node.id() === `${stringifyId(row, col)}`

        if (type !== undefined) {
            judgement &&= node.hasName(`${typeNamePrefix}${type}`)
        }

        if (state !== undefined) {
            judgement &&= node.hasName(state)
        }
        return judgement
    })
}

Jewels.prototype.clearJewels = function (clearable) {
    let t = this,
        promises = []

    t.layer.listening(false)  // prevent any interactions

    // transform clearable:
    // from => [[[row,col,type],[row,col,type],[row,col,type]],...]
    // flatten => [[row,col,type],[row,col,type],[row,col,type],...]
    // and count it in => {[row,col,type]:1,...}
    clearable = _.flatten(clearable)
    let countObject = _.countBy(clearable, c => c.join(","))

    clearable = _.uniqBy(clearable, c => c.join(","))
    clearable = _.sortBy(clearable, [
        c => countObject[c.join(",")],
        c => {
            if (newJewelAddPosition === position.left) {     //drop to left, default on top, smaller row put last for `ascending order`
                return -1 * c[0]
            }
            if (newJewelAddPosition === position.right) {    //drop to right, default on bottom, larger row put last for `ascending order`
                return c[0]
            }
            if (newJewelAddPosition === position.top) {      //drop to top, default on right, larger col put last for `ascending order`
                return c[1]
            }
            if (newJewelAddPosition === position.bottom) {   //drop to bottom, default on left, smaller col put last for `ascending order`
                return -1 * c[1]
            }
        }
    ]) // sort by `clearable count` and `add position` rule( specially for B1)  ascending order 


    while (clearable.length > 0) {

        let self = clearable.pop(), // use `pop` to get and remove the last element
            selfJewel = t._getJewelAt({ row: self[0], col: self[1], type: self[2] }), // it's in clearable so its state must be `jewelState.plain`

            l1 = [self[0], self[1] - 1, self[2]],
            r1 = [self[0], self[1] + 1, self[2]],
            t1 = [self[0] - 1, self[1], self[2]],
            b1 = [self[0] + 1, self[1], self[2]],
            l2 = [self[0], self[1] - 2, self[2]],
            r2 = [self[0], self[1] + 2, self[2]],
            t2 = [self[0] - 2, self[1], self[2]],
            b2 = [self[0] + 2, self[1], self[2]],

            selfCount = countObject[self.join(",")],
            l1Count = countObject[l1.join(",")] || 0,
            r1Count = countObject[r1.join(",")] || 0,
            t1Count = countObject[t1.join(",")] || 0,
            b1Count = countObject[b1.join(",")] || 0,
            l2Count = countObject[l2.join(",")] || 0,
            r2Count = countObject[r2.join(",")] || 0,
            t2Count = countObject[t2.join(",")] || 0,
            b2Count = countObject[b2.join(",")] || 0,

            l1Jewel = l1Count && t._getJewelAt({ row: l1[0], col: l1[1], type: l1[2], state: jewelState.plain }),
            r1Jewel = r1Count && t._getJewelAt({ row: r1[0], col: r1[1], type: r1[2], state: jewelState.plain }),
            t1Jewel = t1Count && t._getJewelAt({ row: t1[0], col: t1[1], type: t1[2], state: jewelState.plain }),
            b1Jewel = b1Count && t._getJewelAt({ row: b1[0], col: b1[1], type: b1[2], state: jewelState.plain }),
            l2Jewel = l2Count && t._getJewelAt({ row: l2[0], col: l2[1], type: l2[2], state: jewelState.plain }),
            r2Jewel = r2Count && t._getJewelAt({ row: r2[0], col: r2[1], type: r2[2], state: jewelState.plain }),
            t2Jewel = t2Count && t._getJewelAt({ row: t2[0], col: t2[1], type: t2[2], state: jewelState.plain }),
            b2Jewel = b2Count && t._getJewelAt({ row: b2[0], col: b2[1], type: b2[2], state: jewelState.plain }),

            smartClearing = function () {
                if (!l1Count) {
                    promises.push(...t._clearJewels([l1Jewel]))
                    _.forEach([l1], p => _.remove(clearable, c => _.isEqual(c, p)))
                } else {
                    promises.push(...t._clearJewels([l1Jewel, l2Jewel]))
                    _.forEach([l1, l2], p => _.remove(clearable, c => _.isEqual(c, p)))
                }

                if (!r1Count) {
                    promises.push(...t._clearJewels([r1Jewel]))
                    _.forEach([r1], p => _.remove(clearable, c => _.isEqual(c, p)))
                } else {
                    promises.push(...t._clearJewels([r1Jewel, r2Jewel]))
                    _.forEach([r1, r2], p => _.remove(clearable, c => _.isEqual(c, p)))
                }

                if (!t1Count) {
                    promises.push(...t._clearJewels([t1Jewel]))
                    _.forEach([t1], p => _.remove(clearable, c => _.isEqual(c, p)))
                } else {
                    promises.push(...t._clearJewels([t1Jewel, t2Jewel]))
                    _.forEach([t1, t2], p => _.remove(clearable, c => _.isEqual(c, p)))
                }

                if (!b1Count) {
                    promises.push(...t._clearJewels([b1Jewel]))
                    _.forEach([b1], p => _.remove(clearable, c => _.isEqual(c, p)))
                } else {
                    promises.push(...t._clearJewels([b1Jewel, b2Jewel]))
                    _.forEach([b1, b2], p => _.remove(clearable, c => _.isEqual(c, p)))
                }

            }

        if (selfCount === 6) {
            promises.push(t._clearAndGenerateJewel(selfJewel, jewelState.blow))
            // clear `8n` @see F1
            smartClearing()
            continue
        }

        if (selfCount === 5) {
            promises.push(t._clearAndGenerateJewel(selfJewel, jewelState.blow))
            // clear `8n` @see E1
            smartClearing()
            continue
        }

        if (selfCount === 4) {
            // clear `8n` @see D1
            if (l1Count && r1Count && t1Count && b1Count) {
                promises.push(t._clearAndGenerateJewel(selfJewel, jewelState.strike))
                smartClearing()
                continue
            }

            // clear `8n` @see D2
            if (!l1Count || !r1Count || !t1Count || !b1Count) {
                promises.push(t._clearAndGenerateJewel(selfJewel, jewelState.blow))
                smartClearing()
                continue
            }

            // clear `8n` @see D3
            if (
                (!l2Count && !r2Count) ||
                (!t2Count && !b2Count)
            ) {
                promises.push(t._clearAndGenerateJewel(selfJewel, jewelState.blow))
                smartClearing()
                continue
            }

            // default clear self only
            promises.push(t._clearJewel(selfJewel))
            continue
        }

        if (selfCount === 3) {
            // clear `8n` @see C1
            if (l1Count && r1Count && t1Count && b1Count) {
                promises.push(t._clearAndGenerateJewel(selfJewel, jewelState.strike))
                smartClearing()
                continue
            }

            // clear `8n` @see C2
            if (
                (!l1Count && r1Count) ||
                (!r1Count && l1Count) ||
                (!t1Count && b1Count) ||
                (!b1Count && t1Count)
            ) {
                promises.push(t._clearAndGenerateJewel(selfJewel, jewelState.strike))
                smartClearing()
                continue
            }

            // clear `8n` @see C3
            if (
                (!l1Count && !r1Count) ||
                (!t1Count && !b1Count)
            ) {
                promises.push(t._clearAndGenerateJewel(selfJewel, jewelState.blow))
                smartClearing()
                continue
            }

            // default clear self only
            promises.push(t._clearJewel(selfJewel))
            continue
        }

        if (selfCount === 2) {

            // clear `8n` @see B1
            if (
                (!l1Count && !r1Count) ||
                (!t1Count && !b1Count)
            ) {
                // B1 swap `currentRowCol` judgement
                if (
                    _.isEqual(t.currentRowCol, [l1[0], l1[1]]) ||
                    _.isEqual(t.currentRowCol, [r1[0], r1[1]]) ||
                    _.isEqual(t.currentRowCol, [t1[0], t1[1]]) ||
                    _.isEqual(t.currentRowCol, [b1[0], b1[1]])
                ) {
                    // do nothing, we have `currentRowCol` in `4c`
                    continue
                } else {
                    // self is `currentRowCol` then clear it and set `currentRowCol` to null
                    if (_.isEqual(t.currentRowCol, [self[0], self[1]])) {
                        t.currentRowCol = null
                    }
                    promises.push(t._clearAndGenerateJewel(selfJewel, jewelState.strike))
                    smartClearing()
                    continue
                }
            }

            // clear `8n` @see B2
            if (
                (!l1Count && r1Count) ||
                (!r1Count && l1Count) ||
                (!t1Count && b1Count) ||
                (!b1Count && t1Count)
            ) {
                promises.push(t._clearAndGenerateJewel(selfJewel, jewelState.strike))
                smartClearing()
                continue
            }

            // clear `8n` @see B3
            if (l1Count && r1Count && t1Count && b1Count) {
                promises.push(t._clearAndGenerateJewel(selfJewel, jewelState.strike))
                smartClearing()
                continue
            }

            // clear `8n` @see B4
            if (
                (!l1Count && !t1Count) ||
                (!l1Count && !b1Count) ||
                (!r1Count && !t1Count) ||
                (!r1Count && !b1Count)
            ) {
                promises.push(t._clearAndGenerateJewel(selfJewel, jewelState.strike))
                smartClearing()
                continue
            }

            // default clear self only
            promises.push(t._clearJewel(selfJewel))
            continue
        }

        if (selfCount === 1) {
            promises.push(t._clearJewel(selfJewel))
        }
    }

    Promise.all(promises).then(() => {
        t.playSound("jc")
        t.addNewJewels()
    })
}

Jewels.prototype.addNewJewels = function () {
    let t = this,
        promises = []

    let existedDropHandleFunc = (missing, row, col) => {
        let foundJewel = t._getJewelAt({ row, col })

        if (foundJewel !== undefined) {
            if (!missing.length) {
                // do nothing
            } else {
                let toRowCol = missing.shift()
                let p = new Promise((resolve, reject) => {
                    foundJewel.to({
                        duration: jewelTweenDuration,
                        onFinish: () => {
                            foundJewel.id(stringifyId(toRowCol[0], toRowCol[1]))
                            resolve()
                        },
                        ...calcJewelPosition(toRowCol[0], toRowCol[1])
                    })
                })
                promises.push(p)
                missing.push([row, col])
            }
        } else {
            missing.push([row, col])
        }
    }

    let fillMissingHandleFunc = (missing, addPosition) => {
        let newJewels = []
        missing.forEach((value, index) => {
            let headOne = newJewels[index - 1],
                exceptType = getJewelType(headOne)   // do not drop  as the same type as the head one

            let asset = t.getRandomJewel([exceptType]),
                newRowCol = []


            if (addPosition === position.left) {
                newRowCol = [value[0], (jewelColCount - 1) + (index + 1)]
            }
            if (addPosition === position.right) {
                newRowCol = [value[0], -1 * (index + 1)]
            }
            if (addPosition === position.top) {
                newRowCol = [(jewelRowCount - 1) + (index + 1), value[1]]
            }
            if (addPosition === position.bottom) {
                newRowCol = [-1 * (index + 1), value[1]]
            }
            let newJewel = t._jewelTemplate(...newRowCol, asset.image, asset.name)

            t.layer.add(newJewel)
            newJewels.push(newJewel)

            let p = new Promise((resolve, reject) => {
                newJewel.to({
                    duration: jewelTweenDuration,
                    onFinish: () => {
                        newJewel.id(stringifyId(value[0], value[1]))
                        resolve()
                    },
                    ...calcJewelPosition(value[0], value[1])
                })
            })
            promises.push(p)
        })
    }

    if (newJewelAddPosition === position.left) {
        // check from top to bottom
        for (let i = 0; i < jewelRowCount; i++) {
            let missing = []
            for (let j = 0; j < jewelColCount; j++) {
                existedDropHandleFunc(missing, i, j)
            }
            if (missing.length) {
                fillMissingHandleFunc(missing, newJewelAddPosition)
            }
        }
    }

    if (newJewelAddPosition === position.right) {
        // check from top to bottom
        for (let i = 0; i < jewelRowCount; i++) {
            let missing = []
            for (let j = jewelColCount - 1; j >= 0; j--) {
                existedDropHandleFunc(missing, i, j)
            }
            if (missing.length) {
                fillMissingHandleFunc(missing, newJewelAddPosition)
            }
        }
    }


    if (newJewelAddPosition === position.top) {
        // check from left to right
        for (let j = 0; j < jewelColCount; j++) {
            let missing = []
            for (let i = 0; i < jewelRowCount; i++) {
                existedDropHandleFunc(missing, i, j)
            }
            if (missing.length) {
                fillMissingHandleFunc(missing, newJewelAddPosition)
            }
        }
    }

    if (newJewelAddPosition === position.bottom) {
        // check from left to right
        for (let j = 0; j < jewelColCount; j++) {
            let missing = []
            for (let i = jewelRowCount - 1; i >= 0; i--) {
                existedDropHandleFunc(missing, i, j)
            }
            if (missing.length) {
                fillMissingHandleFunc(missing, newJewelAddPosition)
            }
        }
    }

    Promise.all(promises).then(() => {
        let clearable = t.checkClearable()
        if (clearable.length) {
            t.clearJewels(clearable)
        } else {
            t.layer.listening(true) // allow interactions now!
            t.layer.drawHit()
        }
    })
}

function getJewelType(jewel) {
    if (jewel === undefined) return undefined

    let names = jewel.name().split(/\s+/g),
        found = _.find(names, n => n.startsWith(typeNamePrefix))
    return found ? Number(found.replace(typeNamePrefix, "")) : undefined
}

function isSameType(...jewels) {
    let types = _.map(jewels, (j) => getJewelType(j)),
        uniqueType = _.uniq(types)
    return uniqueType.length === 1 && _.head(uniqueType) !== undefined
}

Jewels.prototype.checkClearable = function () {
    let t = this,
        clearable = []

    for (let i = 0; i < jewelRowCount; i++) {
        for (let j = 0; j < jewelColCount; j++) {
            // check towards `right` and `bottom`
            let self = t._getJewelAt({ row: i, col: j, state: jewelState.plain }),
                rightOne = t._getJewelAt({ row: i, col: j + 1, state: jewelState.plain }),
                rightTwo = t._getJewelAt({ row: i, col: j + 2, state: jewelState.plain }),
                bottomOne = t._getJewelAt({ row: i + 1, col: j, state: jewelState.plain }),
                bottomTwo = t._getJewelAt({ row: i + 2, col: j, state: jewelState.plain }),
                type = getJewelType(self)

            if (isSameType(self, rightOne, rightTwo)) { // anyone undefined will cause `false`
                clearable.push([[i, j, type], [i, j + 1, type], [i, j + 2, type]])
            }

            if (isSameType(self, bottomOne, bottomTwo)) { // anyone undefined will cause `false`
                clearable.push([[i, j, type], [i + 1, j, type], [i + 2, j, type]])
            }
        }
    }
    return clearable
}

Jewels.prototype.initAssets = function () {
    let t = this
    t.jewelAssets = {}
    t.imageAssets = {}
    t.soundAssets = {}
    t.effectAssets = {}

    return Promise.all([

        Promise.all(
            jewelAssets.map(async (item) => {
                try {
                    const data = await AsyncPreloader.loadItem(item)
                    t.jewelAssets[item.id] = data
                } catch (error) {
                    console.error(`Error loading jewel assets: ${item.id}`, error)
                }
            })
        ),

        Promise.all(
            imageAssets.map(async (item) => {
                try {
                    const data = await AsyncPreloader.loadItem(item)
                    t.imageAssets[item.id] = data
                } catch (error) {
                    console.error(`Error loading image assets: ${item.id}`, error)
                }
            })
        ),

        Promise.all(
            soundAssets.map(async (item) => {
                try {
                    const data = await AsyncPreloader.loadItem(item)
                    audioContext.decodeAudioData(data).then(
                        decodeBuffer => {
                            t.soundAssets[item.id] = decodeBuffer
                        },
                        error => console.error(`Error decoding sound assets: ${item.id}`, error)
                    )
                } catch (error) {
                    console.error(`Error loading sound assets: ${item.id}`, error)
                }
            })
        ),

        Promise.all(
            effectAssets.map(async (item) => {
                try {
                    const data = await AsyncPreloader.loadItem(item)
                    t.effectAssets[item.id] = data
                } catch (error) {
                    console.error(`Error loading effect assets: ${item.id}`, error)
                }
            })
        ),

        AsyncPreloader.loadItem(fontAsset)
            .then(item => {
                // font is ready, we do nothing
            })
            .catch(error => console.error(`Error loading font assets: ${item.id}`, error))
    ])
}

Jewels.prototype.playSound = function (...soundKeys) {
    _.forEach(soundKeys, key => {
        let source = audioContext.createBufferSource()
        source.buffer = this.soundAssets[key]
        source.connect(audioContext.destination)
        source.start(0)
    })
}

Jewels.prototype.useEffect = function (effectKey, image, layer, loop = true) {
    let effectVideo = this.effectAssets[effectKey]
    effectVideo.loop = loop
    image.image(effectVideo)

    let anim = new Konva.Animation(() => { }, layer)
    effectVideo.play()
    anim.start()
    return anim
}


export default Jewels